﻿using System;
using System.Collections.Generic;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Windows.Forms;

namespace aboveUHSReader
{
  internal class UHSFile
  {
    private readonly Font m_propotionalFont = new Font("Lucida Console", 10F, FontStyle.Regular, GraphicsUnit.Point,
                                                       ((0)));

    private Font m_font;
    private Form m_form;
    private PictureBox m_pictureBox;
    private UHSRootNode m_rootNode;
    private TreeNode m_searchTree = new TreeNode("");
    private int m_searchTreePointer = -1;
    private RichTextBoxEx m_textBox;
    private TreeView m_treeView;

    public void Show(Form form, TreeView treeView, RichTextBoxEx textBox, PictureBox pictureBox, string fileName)
    {
      m_form = form;
      m_textBox = textBox;
      m_treeView = treeView;
      m_pictureBox = pictureBox;
      m_font = m_textBox.Font;
      m_textBox.LinkClicked += TextBoxLinkClicked;
      treeView.AfterSelect += TreeViewAfterSelect;
      m_rootNode = ParseFile(fileName, 0);
      if (m_rootNode == null || m_rootNode.Nodes.Count == 0) return;
      treeView.Nodes.Add(m_rootNode.Nodes[0]);
      for (int n = 1; n < m_rootNode.Nodes.Count; ++n)
        m_rootNode.Nodes[0].Nodes.Add(m_rootNode.Nodes[n]);
      treeView.Nodes[0].Expand();
    }

    private static string MakeLink(string link)
    {
      return " " + link + " ";
    }

    private void TextBoxLinkClicked(object sender, LinkClickedEventArgs e)
    {
      var uhsNode = m_treeView.SelectedNode as UHSNode;
      if (uhsNode == null) return;
      foreach (UHSNode node in
        uhsNode.Nodes.OfType<UHSNode>().Where(node => node.Subject == "Link" && MakeLink(node.Content) == e.LinkText))
      {
        m_treeView.SelectedNode = m_rootNode.LinkNode(node.LinkTarget);
        m_treeView.Focus();
        return;
      }
    }

    public void NextSearchResult()
    {
      if (m_searchTree.Nodes.Count == 0) return;
      ++m_searchTreePointer;
      if (m_searchTreePointer == m_searchTree.Nodes.Count)
        m_searchTreePointer = 0;
      var searchNode = m_searchTree.Nodes[m_searchTreePointer] as SearchNode;
      if (searchNode != null)
        m_treeView.SelectedNode = searchNode;
      m_treeView.Focus();
    }

    public void Search(string searchString)
    {
      searchString = searchString.Trim();
      m_searchTree = new TreeNode(searchString);
      Search(m_rootNode.Nodes[0], searchString, m_searchTree);
      m_treeView.Nodes.Add(m_searchTree);
      m_searchTreePointer = -1;
      NextSearchResult();
    }

    private static void Search(TreeNode rootNode, string searchString, TreeNode searchTree)
    {
      foreach (UHSNode node in rootNode.Nodes.OfType<UHSNode>())
        if (node.Contains(searchString))
          searchTree.Nodes.Add(new SearchNode(node));
        else
          Search(node, searchString, searchTree);
    }

    private void TreeViewAfterSelect(object sender, TreeViewEventArgs e)
    {
      var tree = sender as TreeView;
      if (tree == null) return;
      var searchNode = e.Node as SearchNode;
      if (searchNode != null)
      {
        m_treeView.SelectedNode = searchNode.Node;
        return;
      }
      var uhsNode = e.Node as UHSNode;
      if (uhsNode == null) return;
      m_form.Text = uhsNode.Subject;
      m_textBox.Text = uhsNode.Subject != "TextData" ? String.Empty : uhsNode.Content;
      if (uhsNode.Proportional)
      {
        m_textBox.Font = m_propotionalFont;
        m_textBox.WordWrap = false;
      }
      else
      {
        m_textBox.Font = m_font;
        m_textBox.WordWrap = true;
      }

      if (uhsNode.Subject == "Hint" || uhsNode.Subject == "NestHint")
      {
        if (uhsNode.Nodes.OfType<UHSNode>().Count() != 0)
          foreach (UHSNode node in uhsNode.Nodes.OfType<UHSNode>())
            if (node.Subject == "Link")
            {
              if (m_textBox.Text.EndsWith("\n\n"))
                m_textBox.SelectionStart -= 2;
              m_textBox.InsertLink(MakeLink(node.Content));
            }
            else
              m_textBox.SelectedText = String.Format("{0}{1}{1}", node.Content, Environment.NewLine);
        else
          m_textBox.Text = uhsNode.Content;
      }
      if (uhsNode.Subject == "Link") tree.SelectedNode = m_rootNode.LinkNode(uhsNode.LinkTarget);
      if (uhsNode.Subject == "HotSpot" && uhsNode.Nodes.Count == 1)
      {
        var stream = new MemoryStream(((UHSNode) uhsNode.Nodes[0]).ContentToByteArray);
        m_pictureBox.Image = new Bitmap(stream);
        m_pictureBox.Parent.Show();
        m_textBox.Parent.Hide();
      }
      else
      {
        m_pictureBox.Parent.Hide();
        m_textBox.Parent.Show();
      }
    }

    /**
     * Reads a UHS file into an List<string> of text lines and an array of binary bytes.
     * Then call an appropriate parser to construct a UHSRootNode and a tree of UHSNodes.
     * <br />
     * <br />This is likely the only method you'll need.
     *
     * @param fileName file to read
     * @param auxStyle option for 9x files AUX_NORMAL, AUX_IGNORE, or AUX_NEST
     * @return the root of a tree of nodes representing the hint file
     * @see #parse88Format(List<string>, String, int) parse88Format(List<string>, String, int)
     * @see #parse9xFormat(List<string>, byte[], long, int) parse9xFormat(List<string>, byte[], long, int)
     */

    public UHSRootNode ParseFile(String fileName, int auxStyle)
    {
      var uhsFileArray = new List<string>();
      try
      {
        int endHintSection;
        byte[] rawuhs;
        long rawOffset;
        using (var reader = new StreamReader(fileName))
        {
          string tmp = reader.ReadLine();
          if (tmp != "UHS")
          {
            MessageBox.Show(@"Not a UHS file!");
            return null;
          }
          rawOffset = tmp.Length + 2;
          rawOffset += reader.ReadLine().Length + 2;
          //The indeces, from this point, of the first/last lines of hints in 88a files
          //After 88a, those lines contain an "upgrade your reader" notice

          rawOffset += reader.ReadLine().Length + 2;
          tmp = reader.ReadLine();
          rawOffset += tmp.Length + 2;
          endHintSection = Int32.Parse(tmp);

          //There's a hunk of binary referenced by offset at the end of 91a and newer files
          //One can skip to it by searching for 0x1Ah.
          while (!reader.EndOfStream && reader.Peek() != 0x1a)
          {
            tmp = reader.ReadLine();
            rawOffset += tmp.Length + 2;
            uhsFileArray.Add(tmp);
          }
          ++rawOffset;
          var binSize = (int) (reader.BaseStream.Length - rawOffset);
          rawuhs = new byte[binSize];
          reader.BaseStream.Position = rawOffset;
          reader.BaseStream.Read(rawuhs, 0, binSize);
          reader.Close();
        }
        bool version88A = true;
        for (int i = endHintSection; i < uhsFileArray.Count; i++)
        {
          if (uhsFileArray[i] != "** END OF 88A FORMAT **") continue;
          version88A = false;
          //Since v91a, the line count starts here, after the old-style 88a section and its "end of" comment.
          uhsFileArray.RemoveRange(0, i);
          break;
        }
        if (!version88A)
        {
          return Parse9XFormat(uhsFileArray, rawuhs, rawOffset, auxStyle);
        }
      }
      catch (Exception ex)
      {
        MessageBox.Show(ex.Message);
      }
      return null;
    }

    /**
     * Generates a tree of UHSNodes from UHS 91a format onwards.
     * <br />
     * <br />Versions 91a, 95a, and 96a have been seen in the wild.
     * <br />These UHS files are prepended with an 88a section containing an "upgrade your reader" notice.
     * <br />Below, uhsFileArray begins after "** END OF 88A FORMAT **".
     *
     * <pre> UHS
     * # Subject
     * title
     * ...
     * # version
     * title
     * content
     * # incentive
     * title
     * content
     * # info
     * title
     * content
     * 0x1Ah character
     * {binary hunk}
     * </pre>
     * <br />
     * <br />The root node would normally contain up to four children.
     * <br />A 'subject', containing all the subjects, hints, etc., that users care about.
     * <br />A 'version', mentioning the UHS compiler that made the file.
     * <br />An 'info', mentioning the author, publisher, etc.
     * <br />And an 'incentive', listing nodes to show/block if the reader is unregistered.
     * <br />For convenience, these auxiliary nodes can be treated differently.
     *
     * @param uhsFileArray array of all available lines in the file
     * @param rawuhs array of raw bytes at the end of the file
     * @param rawOffset offset to the raw bytes from the beginning of the file
     * @param auxStyle AUX_NORMAL (canon), AUX_IGNORE (omit), or AUX_NEST (move inside the master subject and make that the new root).
     * @return the root of a tree of nodes
     * @see #BuildNodes(List<string>, byte[], long, UHSRootNode, UHSNode, int[], int) BuildNodes(List<string>, byte[], long, UHSRootNode, UHSNode, int[], int)
     */

    public UHSRootNode Parse9XFormat(List<string> uhsFileArray, byte[] rawuhs, long rawOffset, int auxStyle)
    {
      var rootNode = new UHSRootNode();
      try
      {
        //rootNode.Content = "root", UHSNode.STRING);

        String name = uhsFileArray[2]; //This is the title of the master subject node
        int[] key = GenerateKey(name);

        int index = BuildNodes(uhsFileArray, rawuhs, rawOffset, rootNode, rootNode, key, 1);

        while (index < uhsFileArray.Count)
        {
          index += BuildNodes(uhsFileArray, rawuhs, rawOffset, rootNode, rootNode, key, index);
        }
      }
      catch (Exception e)
      {
        MessageBox.Show(e.Message);
      }
      return rootNode;
    }

    /**
     * Generates a decryption key for formats after 88a.
     *
     * @param name the name of the master subject node of the UHS document (not the filename)
     * @return the key
     * @see #decryptNestString(String, int[]) decryptNestString(String, int[])
     * @see #decryptTextHunk(String, int[]) decryptTextHunk(String, int[])
     */

    public int[] GenerateKey(String name)
    {
      var key = new int[name.Length];
      // ReSharper disable CharImplicitlyConvertedToNumeric
      int[] k = {'k', 'e', 'y'};
      // ReSharper restore CharImplicitlyConvertedToNumeric
      for (int i = 0; i < name.Length; i++)
      {
        key[i] = name[i] + (k[i%3] ^ (i + 40));
        while (key[i] > 127)
          key[i] -= 96;
      }
      return key;
    }

    /**
     * Recursively parses UHS newer than 88a.
     * <br />This recognizes various types of hints, and runs specialized methods to decode them.
     * <br />Unrecognized hints are harmlessly omitted.
     *
     * @param uhsFileArray array of all available lines in the file
     * @param rawuhs array of raw bytes at the end of the file
     * @param rawOffset offset to the raw bytes from the beginning of the file
     * @param rootNode an existing root node
     * @param currentNode an existing node to add children to
     * @param key this file's hint decryption key
     * @param startIndex the line number to start parsing from
     * @return the number of lines consumed from the file in parsing children
     */

    public int BuildNodes(List<string> uhsFileArray, byte[] rawuhs, long rawOffset, UHSRootNode rootNode,
                          UHSNode currentNode, int[] key, int startIndex)
    {
      int index = startIndex;
      var regex = new Regex("[0-9]+ [A-Za-z]+$");
      String tmp = uhsFileArray[index];
      if (regex.Match(tmp).Success)
      {
        if (tmp.EndsWith("comment"))
        {
          index += ParseCommentNode(uhsFileArray, rawuhs, rawOffset, rootNode, currentNode, key, index);
        }
        else if (tmp.EndsWith("credit"))
        {
          index += ParseCreditNode(uhsFileArray, rawuhs, rawOffset, rootNode, currentNode, key, index);
        }
        else if (tmp.EndsWith(" hint"))
        {
          index += ParseHintNode(uhsFileArray, rawuhs, rawOffset, rootNode, currentNode, key, index);
        }
        else if (tmp.EndsWith("nesthint"))
        {
          index += ParseNestHintNode(uhsFileArray, rawuhs, rawOffset, rootNode, currentNode, key, index);
        }
        else if (tmp.EndsWith("subject"))
        {
          index += ParseSubjectNode(uhsFileArray, rawuhs, rawOffset, rootNode, currentNode, key, index);
        }
        else if (tmp.EndsWith("link"))
        {
          index += ParseLinkNode(uhsFileArray, rawuhs, rawOffset, rootNode, currentNode, key, index);
        }
        else if (tmp.EndsWith("text"))
        {
          index += ParseTextNode(uhsFileArray, rawuhs, rawOffset, rootNode, currentNode, key, index);
        }
        else if (tmp.EndsWith("hyperpng"))
        {
          index += ParseHyperImgNode(uhsFileArray, rawuhs, rawOffset, rootNode, currentNode, key, index);
        }
        else if (tmp.EndsWith("gifa"))
        {
          index += ParseHyperImgNode(uhsFileArray, rawuhs, rawOffset, rootNode, currentNode, key, index);
        }
        else if (tmp.EndsWith("sound"))
        {
          index += ParseSoundNode(uhsFileArray, rawuhs, rawOffset, rootNode, currentNode, key, index);
        }
        else if (tmp.EndsWith("blank"))
        {
          index += ParseBlankNode(uhsFileArray, rawuhs, rawOffset, rootNode, currentNode, key, index);
        }
        else if (tmp.EndsWith("version"))
        {
          index += ParseVersionNode(uhsFileArray, rawuhs, rawOffset, rootNode, currentNode, key, index);
        }
        else if (tmp.EndsWith("info"))
        {
          index += ParseInfoNode(uhsFileArray, rawuhs, rawOffset, rootNode, currentNode, key, index);
        }
        else if (tmp.EndsWith("incentive"))
        {
          index += ParseIncentiveNode(uhsFileArray, rawuhs, rawOffset, rootNode, currentNode, key, index);
        }
        else
        {
          index += ParseUnknownNode(uhsFileArray, rawuhs, rawOffset, rootNode, currentNode, key, index);
        }
      }
      else
      {
        index++;
      }
      return index - startIndex;
    }

    /**
    * Generates a subject UHSNode and its contents.
    *
    * <pre> # subject
    * title
    * embedded hunk
    * embedded hunk
    * embedded hunk</pre>
    *
    * @param uhsFileArray array of all available lines in the file
    * @param rawuhs array of raw bytes at the end of the file
    * @param rawOffset offset to the raw bytes from the beginning of the file
    * @param rootNode an existing root node
    * @param currentNode an existing node to add children to
    * @param key this file's hint decryption key
    * @param startIndex the line number to start parsing from
    * @return the number of lines consumed from the file in parsing children
    */

    public int ParseSubjectNode(List<string> uhsFileArray, byte[] rawuhs, long rawOffset, UHSRootNode rootNode,
                                UHSNode currentNode, int[] key, int startIndex)
    {
      int index = startIndex;
      String tmp = uhsFileArray[index];
      index++;
      int innerCount = Int32.Parse(tmp.Substring(0, tmp.IndexOf(" "))) - 1;

      var newNode = new UHSNode("Subject") {Content = uhsFileArray[index]};
      ParseTextEscapes(newNode);
      newNode.Id = startIndex;
      currentNode.Add(newNode);
      rootNode.AddLink(newNode);
      index++;
      innerCount--;

      for (int j = 0;
           j < innerCount;
           j += BuildNodes(uhsFileArray, rawuhs, rawOffset, rootNode, newNode, key, index + j))
      {
      }

      index += innerCount;
      return index - startIndex;
    }

    /**
     * Replaces UHS escaped characters in a hint.
     * <br />Escapes have existed from version 88a onwards in most nodes' content and titles.
     * <br />The # character is the main escape char and is written <b>##</b>.
     *
     * <ul><li><b>#</b> a '#' character.</li>
     * <li><b>#a+</b>[AaEeIiOoUu][:'`^]<b>#a-</b> accent enclosed letter; :=diaeresis,'=acute,`=grave,^=curcumflex.</li>
     * <li><b>#a+</b>[Nn]~<b>#a-</b> accent enclosed letter with a tilde.</li>
     * <li><b>#a+</b>ae<b>#a-</b> an ash character.</li>
     * <li><b>#a+</b>TM<b>#a-</b> a trademark character.</li>
     * <li><b>#w.</b> raw newlines are spaces.</li>
     * <li><b>#w+</b> raw newlines are spaces (default).</li>
     * <li><b>#w-</b> raw newlines are newlines.</li></ul>
     *
     * The following are left for display code to handle (e.g., UHSTextArea).
     * <ul><li><b>#p+</b> proportional font (default).</li>
     * <li><b>#p-</b> non-proportional font.</li></ul>
     *
     * This is displayed, but not a clickable hyperlink.
     * <ul><li><b>#h+</b> through <b>#h-</b> is a hyperlink (http or email).</li></ul>
     * <br />Illustrative UHS: <i>Portal: Achievements</i> (hyperlink)
     *
     * @param currentNode the node whose content needs replacing
     */

    public void ParseTextEscapes(UHSNode currentNode)
    {
      String breakStr = " ";
      const string linebreak = "^break^";
      const string accentPrefix = "#a+";
      const string accentSuffix = "#a-";
      const string wspcA = "#w+";
      const string wspcB = "#w.";
      const string wnlin = "#w-";

      var buf = new StringBuilder();

      for (int c = 0; c < currentNode.Content.Length; c++)
      {
        if (c + 1 < currentNode.Content.Length)
        {
          if (currentNode.Content[c] == '#' && currentNode.Content[c + 1] == '#')
          {
            buf.Append('#');
            c += 1;
            continue;
          }
        }
        string chunkA;
        if (currentNode.Content[c] == '#')
        {
          if (c + 7 < currentNode.Content.Length)
          {
            chunkA = currentNode.Content.Substring(c, 3);
            string chunkB = currentNode.Content.Substring(c + 5, 3);
            if (chunkA == accentPrefix && chunkB == accentSuffix)
            {
              switch (currentNode.Content[c + 4])
              {
                case ':':
                  if (currentNode.Content[c + 3] == 'A')
                  {
                    buf.Append('Ä');
                    c += 7;
                    continue;
                  }
                  if (currentNode.Content[c + 3] == 'E')
                  {
                    buf.Append('Ë');
                    c += 7;
                    continue;
                  }
                  if (currentNode.Content[c + 3] == 'I')
                  {
                    buf.Append('Ï');
                    c += 7;
                    continue;
                  }
                  if (currentNode.Content[c + 3] == 'O')
                  {
                    buf.Append('Ö');
                    c += 7;
                    continue;
                  }
                  if (currentNode.Content[c + 3] == 'U')
                  {
                    buf.Append('Ü');
                    c += 7;
                    continue;
                  }
                  if (currentNode.Content[c + 3] == 'a')
                  {
                    buf.Append('ä');
                    c += 7;
                    continue;
                  }
                  if (currentNode.Content[c + 3] == 'e')
                  {
                    buf.Append('ë');
                    c += 7;
                    continue;
                  }
                  if (currentNode.Content[c + 3] == 'i')
                  {
                    buf.Append('ï');
                    c += 7;
                    continue;
                  }
                  if (currentNode.Content[c + 3] == 'o')
                  {
                    buf.Append('ö');
                    c += 7;
                    continue;
                  }
                  if (currentNode.Content[c + 3] == 'u')
                  {
                    buf.Append('ü');
                    c += 7;
                    continue;
                  }
                  break;
                case '\'':
                  if (currentNode.Content[c + 3] == 'A')
                  {
                    buf.Append('Á');
                    c += 7;
                    continue;
                  }
                  if (currentNode.Content[c + 3] == 'E')
                  {
                    buf.Append('É');
                    c += 7;
                    continue;
                  }
                  if (currentNode.Content[c + 3] == 'I')
                  {
                    buf.Append('Í');
                    c += 7;
                    continue;
                  }
                  if (currentNode.Content[c + 3] == 'O')
                  {
                    buf.Append('Ó');
                    c += 7;
                    continue;
                  }
                  if (currentNode.Content[c + 3] == 'U')
                  {
                    buf.Append('Ú');
                    c += 7;
                    continue;
                  }
                  if (currentNode.Content[c + 3] == 'a')
                  {
                    buf.Append('á');
                    c += 7;
                    continue;
                  }
                  if (currentNode.Content[c + 3] == 'e')
                  {
                    buf.Append('é');
                    c += 7;
                    continue;
                  }
                  if (currentNode.Content[c + 3] == 'i')
                  {
                    buf.Append('í');
                    c += 7;
                    continue;
                  }
                  if (currentNode.Content[c + 3] == 'o')
                  {
                    buf.Append('ó');
                    c += 7;
                    continue;
                  }
                  if (currentNode.Content[c + 3] == 'u')
                  {
                    buf.Append('ú');
                    c += 7;
                    continue;
                  }
                  break;
                case '`':
                  if (currentNode.Content[c + 3] == 'A')
                  {
                    buf.Append('À');
                    c += 7;
                    continue;
                  }
                  if (currentNode.Content[c + 3] == 'E')
                  {
                    buf.Append('È');
                    c += 7;
                    continue;
                  }
                  if (currentNode.Content[c + 3] == 'I')
                  {
                    buf.Append('Ì');
                    c += 7;
                    continue;
                  }
                  if (currentNode.Content[c + 3] == 'O')
                  {
                    buf.Append('Ò');
                    c += 7;
                    continue;
                  }
                  if (currentNode.Content[c + 3] == 'U')
                  {
                    buf.Append('Ù');
                    c += 7;
                    continue;
                  }
                  if (currentNode.Content[c + 3] == 'a')
                  {
                    buf.Append('à');
                    c += 7;
                    continue;
                  }
                  if (currentNode.Content[c + 3] == 'e')
                  {
                    buf.Append('è');
                    c += 7;
                    continue;
                  }
                  if (currentNode.Content[c + 3] == 'i')
                  {
                    buf.Append('ì');
                    c += 7;
                    continue;
                  }
                  if (currentNode.Content[c + 3] == 'o')
                  {
                    buf.Append('ò');
                    c += 7;
                    continue;
                  }
                  if (currentNode.Content[c + 3] == 'u')
                  {
                    buf.Append('ù');
                    c += 7;
                    continue;
                  }
                  break;
                case '^':
                  if (currentNode.Content[c + 3] == 'A')
                  {
                    buf.Append('Â');
                    c += 7;
                    continue;
                  }
                  if (currentNode.Content[c + 3] == 'E')
                  {
                    buf.Append('Ê');
                    c += 7;
                    continue;
                  }
                  if (currentNode.Content[c + 3] == 'I')
                  {
                    buf.Append('Î');
                    c += 7;
                    continue;
                  }
                  if (currentNode.Content[c + 3] == 'O')
                  {
                    buf.Append('Ô');
                    c += 7;
                    continue;
                  }
                  if (currentNode.Content[c + 3] == 'U')
                  {
                    buf.Append('Û');
                    c += 7;
                    continue;
                  }
                  if (currentNode.Content[c + 3] == 'a')
                  {
                    buf.Append('â');
                    c += 7;
                    continue;
                  }
                  if (currentNode.Content[c + 3] == 'e')
                  {
                    buf.Append('ê');
                    c += 7;
                    continue;
                  }
                  if (currentNode.Content[c + 3] == 'i')
                  {
                    buf.Append('î');
                    c += 7;
                    continue;
                  }
                  if (currentNode.Content[c + 3] == 'o')
                  {
                    buf.Append('ô');
                    c += 7;
                    continue;
                  }
                  if (currentNode.Content[c + 3] == 'u')
                  {
                    buf.Append('û');
                    c += 7;
                    continue;
                  }
                  break;
                case '~':
                  if (currentNode.Content[c + 3] == 'N')
                  {
                    buf.Append('Ñ');
                    c += 7;
                    continue;
                  }
                  if (currentNode.Content[c + 3] == 'n')
                  {
                    buf.Append('ñ');
                    c += 7;
                    continue;
                  }
                  break;
                default:
                  if (currentNode.Content[c + 3] == 'a' && currentNode.Content[c + 4] == 'e')
                  {
                    buf.Append('æ');
                    c += 7;
                    continue;
                  }
                  if (currentNode.Content[c + 3] == 'T' && currentNode.Content[c + 4] == 'M')
                  {
                    buf.Append('™');
                    c += 7;
                    continue;
                  }
                  MessageBox.Show(@"Unknown accent: " + currentNode.Content[c + 3] + currentNode.Content[c + 4]);
                  break;
              }
            }
          }
          if (c + 2 < currentNode.Content.Length)
          {
            chunkA = currentNode.Content.Substring(c, 3);
            if (chunkA == wspcA || chunkA == wspcB)
            {
              breakStr = " ";
              c += 2;
              continue;
            }
            if (chunkA == wnlin)
            {
              breakStr = "\n";
              c += 2;
              continue;
            }
          }
        }
        if (c + 6 < currentNode.Content.Length)
        {
          chunkA = currentNode.Content.Substring(c, 7);
          if (chunkA == linebreak)
          {
            buf.Append(breakStr);
            c += 6;
            continue;
          }
        }
        buf.Append(currentNode.Content[c]);
      }

      currentNode.Content = buf.ToString();
    }

    private static string BytesToString(IEnumerable<byte> bytes)
    {
      var stringBuilder = new StringBuilder();
      foreach (byte b in bytes)
        stringBuilder.Append((Char) b);
      return stringBuilder.ToString();
    }

    /**
     * Generates a text UHSNode.
     *
     * <pre> # text
     * title
     * 000000 0 offset Length</pre>
     *
     * @param uhsFileArray array of all available lines in the file
     * @param rawuhs array of raw bytes at the end of the file
     * @param rawOffset offset to the raw bytes from the beginning of the file
     * @param rootNode an existing root node
     * @param currentNode an existing node to add children to
     * @param key this file's hint decryption key
     * @param startIndex the line number to start parsing from
     * @return the number of lines consumed from the file in parsing children
     * @see #decryptTextHunk(String, int[]) decryptTextHunk(String, int[])
     */

    public int ParseTextNode(List<string> uhsFileArray, byte[] rawuhs, long rawOffset, UHSRootNode rootNode,
                             UHSNode currentNode, int[] key, int startIndex)
    {
      const string breakChar = "\n";

      int index = startIndex;

      index++;
      // int innerCount = Int32.Parse(tmp.Substring(0, tmp.IndexOf(" "))) - 1;

      var textNode = new UHSNode("Text") {Content = uhsFileArray[index]};
      ParseTextEscapes(textNode);
      textNode.Id = startIndex;
      currentNode.Add(textNode);
      rootNode.AddLink(textNode);
      index++;

      string tmp = uhsFileArray[index];
      index++;
      long offset = long.Parse(tmp.Substring(9, tmp.LastIndexOf(" ") - 9)) - rawOffset;
      int length = Int32.Parse(tmp.Substring(tmp.LastIndexOf(" ") + 1));

      var tmpContent = new StringBuilder();
      var newNode = new UHSNode("TextData");

      byte[] tmpBytes = null;
      if (rawOffset != -1) tmpBytes = ReadBinaryHunk(rawuhs, offset, length);
      if (tmpBytes != null)
      {
        tmp = BytesToString(tmpBytes);
      }
      else
      {
        MessageBox.Show(@"Could not read referenced raw bytes");
        tmp = "";
      }

      String[] lines = Regex.Split(tmp, "(\r\n)|\r|\n");
      foreach (string t in lines)
      {
        if (t.Trim().Length == 0) continue;
        if (tmpContent.Length > 0) tmpContent.Append(breakChar);
        tmpContent.Append(DecryptTextHunk(t, key));
      }
      newNode.Content = tmpContent.ToString();
      ParseTextEscapes(newNode);
      textNode.Add(newNode);

      return index - startIndex;
    }

    /**
     * Reads some raw bytes originally from the end of a UHS file.
     * <br />Images, comments, sounds, etc., are stored there.
     *
     * This offset is relative to the start of the raw bytes, not the beginning of the file.
     *
     * @param rawuhs array of bytes at the end of the file (after 0x1Ah)
     * @param offset starting index within the array (must be less than Integer.MAX_VALUE)
     * @param Length the desired number of bytes to retrieve
     * @return the relevant bytes, or null if the offset or Length is invalid
     */

    public byte[] ReadBinaryHunk(byte[] rawuhs, long offset, int length)
    {
      if (offset < 0 || offset > Int32.MaxValue || length < 0 || length > rawuhs.Length)
        return null;
      var result = new byte[length];
      for (int i = 0; i < length; i++)
      {
        result[i] = rawuhs[(int) offset + i];
      }
      return result;
    }

    /**
     * Decrypts the content of 'text' hunks.
     * <br />This is only necessary when initially parsing a file.
     *
     * @param input ciphertext
     * @param key this file's hint decryption key
     * @return the decrypted text
     */

    public String DecryptTextHunk(String input, int[] key)
    {
      if (input.Trim().Length == 0) return "";
      var tmp = new StringBuilder(input.Length);

      for (int i = 0; i < input.Length; i++)
      {
        int codeoffset = i%key.Length;
        int tmpChar = input[i] - (key[codeoffset] ^ (codeoffset + 40));
        while (tmpChar < 32)
        {
          tmpChar += 96;
        }
        tmp.Append((char) tmpChar);
      }

      return tmp.ToString();
    }

    /**
     * Generates a blank UHSNode for spacing.
     *
     * @param uhsFileArray array of all available lines in the file
     * @param rawuhs array of raw bytes at the end of the file
     * @param rawOffset offset to the raw bytes from the beginning of the file
     * @param rootNode an existing root node
     * @param currentNode an existing node to add children to
     * @param key this file's hint decryption key
     * @param startIndex the line number to start parsing from
     * @return the number of lines consumed from the file in parsing children
     */

    public int ParseBlankNode(List<string> uhsFileArray, byte[] rawuhs, long rawOffset, UHSRootNode rootNode,
                              UHSNode currentNode, int[] key, int startIndex)
    {
      int index = startIndex;
      String tmp = uhsFileArray[index];
      index++;
      int innerCount = Int32.Parse(tmp.Substring(0, tmp.IndexOf(" "))) - 1;

      var newNode = new UHSNode("Blank") {Content = "---------"};
      currentNode.Add(newNode);

      index += innerCount;
      return index - startIndex;
    }

    /**
     * Generates a version UHSNode.
     * This is the version reported by the hint file.
     * It may be inaccurate, blank, or conflict with what is claimed in the info node.
     *
     * <pre> # version
     * title
     * sentence
     * sentence
     * sentence</pre>
     *
     * <br />Illustrative UHS: <i>Frankenstein: Through the Eyes of the Monster (blank version)</i>
     * <br />Illustrative UHS: <i>Kingdom O' Magic (blank version)</i>
     * <br />Illustrative UHS: <i>Out of This World (blank version)</i>
     * <br />Illustrative UHS: <i>Spycraft: The Great Game (blank version)</i>
     * <br />Illustrative UHS: <i>Star Control 3 (blank version)</i>
     * <br />Illustrative UHS: <i>System Shock (blank version)</i>
     * <br />Illustrative UHS: <i>The Bizarre Adventures of Woodruff (blank version)</i>
     *
     * @param uhsFileArray array of all available lines in the file
     * @param rawuhs array of raw bytes at the end of the file
     * @param rawOffset offset to the raw bytes from the beginning of the file
     * @param rootNode an existing root node
     * @param currentNode an existing node to add children to
     * @param key this file's hint decryption key
     * @param startIndex the line number to start parsing from
     * @return the number of lines consumed from the file in parsing children
     */

    public int ParseVersionNode(List<string> uhsFileArray, byte[] rawuhs, long rawOffset, UHSRootNode rootNode,
                                UHSNode currentNode, int[] key, int startIndex)
    {
      const string breakChar = " ";

      int index = startIndex;
      String tmp = uhsFileArray[index];
      index++;
      int innerCount = Int32.Parse(tmp.Substring(0, tmp.IndexOf(" "))) - 1;

      var versionNode = new UHSNode("Version") {Content = "Version: " + uhsFileArray[index]};
      ParseTextEscapes(versionNode);
      versionNode.Id = startIndex;
      currentNode.Add(versionNode);
      rootNode.AddLink(versionNode);
      index++;
      innerCount--;

      var tmpContent = new StringBuilder();
      var newNode = new UHSNode("VersionData");

      for (int j = 0; j < innerCount; j++)
      {
        if (tmpContent.Length > 0) tmpContent.Append(breakChar);
        tmpContent.Append(uhsFileArray[index + j]);
      }
      newNode.Content = tmpContent.ToString();
      ParseTextEscapes(newNode);
      versionNode.Add(newNode);

      index += innerCount;
      return index - startIndex;
    }


    /**
     * Generates an info UHSNode.
     *
     * <pre> # info
     * -
     * Length=#######
     * date=DD-Mon-YY
     * time=24:00:00
     * author=name
     * publisher=name
     * copyright=sentence
     * copyright=sentence
     * copyright=sentence
     * >sentence
     * >sentence
     * >sentence</pre>
     *
     * @param uhsFileArray array of all available lines in the file
     * @param rawuhs array of raw bytes at the end of the file
     * @param rawOffset offset to the raw bytes from the beginning of the file
     * @param rootNode an existing root node
     * @param currentNode an existing node to add children to
     * @param key this file's hint decryption key
     * @param startIndex the line number to start parsing from
     * @return the number of lines consumed from the file in parsing children
     */

    public int ParseInfoNode(List<string> uhsFileArray, byte[] rawuhs, long rawOffset, UHSRootNode rootNode,
                             UHSNode currentNode, int[] key, int startIndex)
    {
      int index = startIndex;
      String tmp = uhsFileArray[index];
      index++;
      int innerCount = Int32.Parse(tmp.Substring(0, tmp.IndexOf(" "))) - 1;

      var infoNode = new UHSNode("Info") {Content = "Info: " + uhsFileArray[index], Id = startIndex};
      currentNode.Add(infoNode);
      rootNode.AddLink(infoNode);
      index++;
      innerCount--;

      if (innerCount > 0)
      {
        var lengthBuf = new StringBuilder();
        var dateBuf = new StringBuilder();
        var timeBuf = new StringBuilder();
        var authorBuf = new StringBuilder();
        var publisherBuf = new StringBuilder();
        var copyrightBuf = new StringBuilder();
        var authorNoteBuf = new StringBuilder();
        var gameNoteBuf = new StringBuilder();
        var noticeBuf = new StringBuilder();

        var unknownBuf = new StringBuilder();
        var buffers = new[]
                        {
                          lengthBuf, dateBuf, timeBuf, authorBuf, publisherBuf, copyrightBuf, authorNoteBuf, gameNoteBuf
                          ,
                          noticeBuf, unknownBuf
                        };

        var tmpContent = new StringBuilder();
        StringBuilder currentBuffer;
        var newNode = new UHSNode("InfoData");

        for (int j = 0; j < innerCount; j++)
        {
          tmp = uhsFileArray[index + j];
          String breakChar;
          if (tmp.StartsWith("copyright") || tmp.StartsWith("notice") || tmp.StartsWith("author-note") ||
              tmp.StartsWith("game-note") || tmp.StartsWith(">")) breakChar = " ";
          else breakChar = "\n";

          if (tmp.StartsWith("length="))
          {
            currentBuffer = lengthBuf;
            //tmp = tmp.Substring(7);
          }
          else if (tmp.StartsWith("date="))
          {
            currentBuffer = dateBuf;
            //tmp = tmp.Substring(5);
          }
          else if (tmp.StartsWith("time="))
          {
            currentBuffer = timeBuf;
            //tmp = tmp.Substring(5);
          }
          else if (tmp.StartsWith("author="))
          {
            currentBuffer = authorBuf;
            //tmp = tmp.Substring(7);
          }
          else if (tmp.StartsWith("publisher="))
          {
            currentBuffer = publisherBuf;
            //tmp = tmp.Substring(10);
          }
          else if (tmp.StartsWith("copyright="))
          {
            currentBuffer = copyrightBuf;
            if (currentBuffer.Length == 0)
            {
              currentBuffer.Append("copyright=");
              breakChar = "";
            }
            tmp = tmp.Substring(10);
          }
          else if (tmp.StartsWith("author-note="))
          {
            currentBuffer = authorNoteBuf;
            if (currentBuffer.Length == 0)
            {
              currentBuffer.Append("author-note=");
              breakChar = "";
            }
            tmp = tmp.Substring(12);
          }
          else if (tmp.StartsWith("game-note="))
          {
            currentBuffer = gameNoteBuf;
            if (currentBuffer.Length == 0)
            {
              currentBuffer.Append("game-note=");
              breakChar = "";
            }
            tmp = tmp.Substring(10);
          }
          else if (tmp.StartsWith(">"))
          {
            currentBuffer = noticeBuf;
            tmp = tmp.Substring(1);
          }
          else
          {
            currentBuffer = unknownBuf;
            MessageBox.Show(@"Unknown Info hunk line: " + tmp);
          }

          if (currentBuffer.Length > 0) currentBuffer.Append(breakChar);
          currentBuffer.Append(tmp);
        }

        for (int i = 0; i < buffers.Length; i++)
        {
          if (buffers[i].Length == 0) continue;
          if (tmpContent.Length > 0)
          {
            tmpContent.Append("\n");
            if (i == 5 || i == 6 || i == 7 || i == 8) tmpContent.Append("\n");
          }
          tmpContent.Append(buffers[i]);
        }

        newNode.Content = tmpContent.ToString();
        infoNode.Add(newNode);
      }

      index += innerCount;
      return index - startIndex;
    }


    /**
     * Generates an incentive UHSNode.
     * <br />This node lists IDs to show/block if the reader is unregistered.
     * <br />
     * <br />The list is a space separated string of numbers, each with 'Z' or 'A' appended.
     * <br />'Z' means hide in registered readers, but show in unregistered ones.
     * <br />'A' means deny access in unregistered readers.
     * <br />In some files, there is no list, and the node only occupies 2 lines.
     *
     * <pre> # incentive
     * -
     * ID list (encrypted)</pre>
     *
     * @param uhsFileArray array of all available lines in the file
     * @param rawuhs array of raw bytes at the end of the file
     * @param rawOffset offset to the raw bytes from the beginning of the file
     * @param rootNode an existing root node
     * @param currentNode an existing node to add children to
     * @param key this file's hint decryption key
     * @param startIndex the line number to start parsing from
     * @return the number of lines consumed from the file in parsing children
     */

    public int ParseIncentiveNode(List<string> uhsFileArray, byte[] rawuhs, long rawOffset, UHSRootNode rootNode,
                                  UHSNode currentNode, int[] key, int startIndex)
    {
      int index = startIndex;
      String tmp = uhsFileArray[index];
      index++;
      int innerCount = Int32.Parse(tmp.Substring(0, tmp.IndexOf(" "))) - 1;

      var incentiveNode = new UHSNode("Incentive") {Content = "Incentive: " + uhsFileArray[index], Id = startIndex};
      currentNode.Add(incentiveNode);
      rootNode.AddLink(incentiveNode);
      index++;
      innerCount--;

      if (innerCount > 0)
      {
        tmp = uhsFileArray[index];
        index++;
        var newNode = new UHSNode("IncentiveData") {Content = DecryptNestString(tmp, key)};
        incentiveNode.Add(newNode);
      }

      return index - startIndex;
    }


    /**
     * Generates a stand-in UHSNode for an unknown hunk.
     *
     * @param uhsFileArray array of all available lines in the file
     * @param rawuhs array of raw bytes at the end of the file
     * @param rawOffset offset to the raw bytes from the beginning of the file
     * @param rootNode an existing root node
     * @param currentNode an existing node to add children to
     * @param key this file's hint decryption key
     * @param startIndex the line number to start parsing from
     * @return the number of lines consumed from the file in parsing children
     */

    public int ParseUnknownNode(List<string> uhsFileArray, byte[] rawuhs, long rawOffset, UHSRootNode rootNode,
                                UHSNode currentNode, int[] key, int startIndex)
    {
      int index = startIndex;
      String tmp = uhsFileArray[index];
      index++;
      int innerCount = Int32.Parse(tmp.Substring(0, tmp.IndexOf(" "))) - 1;

      MessageBox.Show(@"Unknown Hunk: " + tmp);

      var newNode = new UHSNode("Unknown") {Content = "^UNKNOWN HUNK^"};
      currentNode.Add(newNode);

      index += innerCount;
      return index - startIndex;
    }

    /**
     * Decrypts the content of 'nesthint' and 'incentive' hunks.
     * <br />This is only necessary when initially parsing a file.
     *
     * @param input ciphertext
     * @param key this file's hint decryption key
     * @return the decrypted text
     */

    public String DecryptNestString(String input, int[] key)
    {
      var tmp = new StringBuilder(input.Length);

      for (int i = 0; i < input.Length; i++)
      {
        int codeoffset = i%key.Length;
        int tmpChar = input[i] - (key[codeoffset] ^ (i + 40));
        while (tmpChar < 32)
        {
          tmpChar += 96;
        }
        tmp.Append((char) tmpChar);
      }

      return tmp.ToString();
    }

    /**
 * Generates a nested hint UHSNode and its contents.
 *
 * <pre> # nesthint
 * Question
 * hint (encrypted)
 * -
 * partial hint (encrypted)
 * =
 * embedded hunk
 * rest of hint (encrypted)
 * -
 * hint (encrypted)</pre>
 *
 * @param uhsFileArray array of all available lines in the file
 * @param rawuhs array of raw bytes at the end of the file
 * @param rawOffset offset to the raw bytes from the beginning of the file
 * @param rootNode an existing root node
 * @param currentNode an existing node to add children to
 * @param key this file's hint decryption key
 * @param startIndex the line number to start parsing from
 * @return the number of lines consumed from the file in parsing children
 * @see #decryptNestString(String, int[]) decryptNestString(String, int[])
 */

    public int ParseNestHintNode(List<string> uhsFileArray, byte[] rawuhs, long rawOffset, UHSRootNode rootNode,
                                 UHSNode currentNode, int[] key, int startIndex)
    {
      const string breakChar = "^break^";

      int index = startIndex;
      String tmp = uhsFileArray[index];
      index++;
      int innerCount = Int32.Parse(tmp.Substring(0, tmp.IndexOf(" "))) - 1;

      var hintNode = new UHSNode("NestHint") {Content = uhsFileArray[index]};
      ParseTextEscapes(hintNode);
      hintNode.Id = startIndex;
      currentNode.Add(hintNode);
      rootNode.AddLink(hintNode);
      index++;
      innerCount--;

      var tmpContent = new StringBuilder();
      var newNode = new UHSNode("Hint");

      for (int j = 0; j < innerCount; j++)
      {
        tmp = uhsFileArray[index + j];
        if (tmp == "-")
        {
          //A hint, add last content
          if (tmpContent.Length > 0)
          {
            newNode.Content = tmpContent.ToString();
            ParseTextEscapes(newNode);
            hintNode.Add(newNode);
            newNode = new UHSNode("Hint");
            tmpContent.Remove(0, tmpContent.Length);
          }
        }
        else if (tmp == "=")
        {
          //Nested hunk, add last content
          if (tmpContent.Length > 0)
          {
            newNode.Content = tmpContent.ToString();
            ParseTextEscapes(newNode);
            hintNode.Add(newNode);
          }

          j += BuildNodes(uhsFileArray, rawuhs, rawOffset, rootNode, hintNode, key, index + j + 1);

          if (tmpContent.Length > 0)
          {
            newNode = new UHSNode("Hint");
            tmpContent.Remove(0, tmpContent.Length);
          }
        }
        else
        {
          if (tmpContent.Length > 0) tmpContent.Append(breakChar);
          tmpContent.Append(DecryptNestString(uhsFileArray[index + j], key));
        }

        if (j == innerCount - 1 && tmpContent.Length > 0)
        {
          newNode.Content = tmpContent.ToString();
          ParseTextEscapes(newNode);
          hintNode.Add(newNode);
        }
      }

      index += innerCount;
      return index - startIndex;
    }

/**
 * Generates a normal hint UHSNode.
 *
 * <pre> # hint
 * Question
 * hint (encrypted)
 * -
 * hint (encrypted)</pre>
 *
 * @param uhsFileArray array of all available lines in the file
 * @param rawuhs array of raw bytes at the end of the file
 * @param rawOffset offset to the raw bytes from the beginning of the file
 * @param rootNode an existing root node
 * @param currentNode an existing node to add children to
 * @param key this file's hint decryption key
 * @param startIndex the line number to start parsing from
 * @return the number of lines consumed from the file in parsing children
 * @see #decryptString(String) decryptNestString(String)
 */

    public int ParseHintNode(List<string> uhsFileArray, byte[] rawuhs, long rawOffset, UHSRootNode rootNode,
                             UHSNode currentNode, int[] key, int startIndex)
    {
      const string breakChar = "^break^";

      int index = startIndex;
      String tmp = uhsFileArray[index];
      index++;
      int innerCount = Int32.Parse(tmp.Substring(0, tmp.IndexOf(" "))) - 1 - 1;

      var hintNode = new UHSNode("Hint") {Content = uhsFileArray[index]};
      ParseTextEscapes(hintNode);
      hintNode.Id = startIndex;
      currentNode.Add(hintNode);
      rootNode.AddLink(hintNode);
      index++;

      var tmpContent = new StringBuilder();
      var newNode = new UHSNode("Hint");

      for (int j = 0; j < innerCount; j++)
      {
        tmp = uhsFileArray[index + j];
        if (tmp == "-")
        {
          if (tmpContent.Length > 0)
          {
            newNode.Content = tmpContent.ToString();
            ParseTextEscapes(newNode);
            hintNode.Add(newNode);
            newNode = new UHSNode("Hint");
            tmpContent.Remove(0, tmpContent.Length);
          }
        }
        else
        {
          if (tmpContent.Length > 0) tmpContent.Append(breakChar);

          tmp = uhsFileArray[index + j];
          tmpContent.Append(tmp == " " ? "\n \n" : DecryptString(tmp));
        }

        if (j == innerCount - 1 && tmpContent.Length > 0)
        {
          newNode.Content = tmpContent.ToString();
          ParseTextEscapes(newNode);
          hintNode.Add(newNode);
        }
      }

      index += innerCount;
      return index - startIndex;
    }

    /**
     * Generates a comment UHSNode.
     *
     * <pre> # comment
     * title
     * sentence
     * sentence
     * sentence</pre>
     *
     * @param uhsFileArray array of all available lines in the file
     * @param rawuhs array of raw bytes at the end of the file
     * @param rawOffset offset to the raw bytes from the beginning of the file
     * @param rootNode an existing root node
     * @param currentNode an existing node to add children to
     * @param key this file's hint decryption key
     * @param startIndex the line number to start parsing from
     * @return the number of lines consumed from the file in parsing children
     */

    public int ParseCommentNode(List<string> uhsFileArray, byte[] rawuhs, long rawOffset, UHSRootNode rootNode,
                                UHSNode currentNode, int[] key, int startIndex)
    {
      const string breakChar = " ";

      int index = startIndex;
      String tmp = uhsFileArray[index];
      index++;
      int innerCount = Int32.Parse(tmp.Substring(0, tmp.IndexOf(" "))) - 1;

      var commentNode = new UHSNode("Comment") {Content = uhsFileArray[index]};
      ParseTextEscapes(commentNode);
      commentNode.Id = startIndex;
      currentNode.Add(commentNode);
      rootNode.AddLink(commentNode);
      index++;
      innerCount--;

      var tmpContent = new StringBuilder();
      var newNode = new UHSNode("CommentData");

      for (int j = 0; j < innerCount; j++)
      {
        if (tmpContent.Length > 0) tmpContent.Append(breakChar);
        tmpContent.Append(uhsFileArray[index + j]);
      }
      newNode.Content = tmpContent.ToString();
      ParseTextEscapes(newNode);
      commentNode.Add(newNode);

      index += innerCount;
      return index - startIndex;
    }

    /**
     * Generates a credit UHSNode.
     *
     * <pre> # credit
     * title
     * sentence
     * sentence
     * sentence</pre>
     *
     * @param uhsFileArray array of all available lines in the file
     * @param rawuhs array of raw bytes at the end of the file
     * @param rawOffset offset to the raw bytes from the beginning of the file
     * @param rootNode an existing root node
     * @param currentNode an existing node to add children to
     * @param key this file's hint decryption key
     * @param startIndex the line number to start parsing from
     * @return the number of lines consumed from the file in parsing children
     */

    public int ParseCreditNode(List<string> uhsFileArray, byte[] rawuhs, long rawOffset, UHSRootNode rootNode,
                               UHSNode currentNode, int[] key, int startIndex)
    {
      const string breakChar = " ";

      int index = startIndex;
      String tmp = uhsFileArray[index];
      index++;
      int innerCount = Int32.Parse(tmp.Substring(0, tmp.IndexOf(" "))) - 1;

      var creditNode = new UHSNode("Credit") {Content = uhsFileArray[index]};
      ParseTextEscapes(creditNode);
      creditNode.Id = startIndex;
      currentNode.Add(creditNode);
      rootNode.AddLink(creditNode);
      index++;
      innerCount--;

      var tmpContent = new StringBuilder();
      var newNode = new UHSNode("CreditData");

      for (int j = 0; j < innerCount; j++)
      {
        if (tmpContent.Length > 0) tmpContent.Append(breakChar);
        tmpContent.Append(uhsFileArray[index + j]);
      }
      newNode.Content = tmpContent.ToString();
      ParseTextEscapes(newNode);
      creditNode.Add(newNode);

      index += innerCount;
      return index - startIndex;
    }

    /**
     * Decrypts the content of standalone 'hint' hunks, and all 88a blocks.
     * <br />This is only necessary when initially parsing a file.
     *
     * @param input ciphertext
     * @return the decrypted text
     */

    public String DecryptString(String input)
    {
      var tmp = new StringBuilder(input.Length);

      foreach (int mychar in input)
      {
        if (mychar < 32)
          tmp.Append((char) mychar);
        else if (mychar < 80)
          tmp.Append((char) (mychar*2 - 32));
        else
          tmp.Append((char) (mychar*2 - 127));
      }

      return tmp.ToString();
    }

    /**
      * Generates a link UHSNode.
      * <br />Nodes like this that have link targets behave like conventional hyperlinks instead of containing child nodes.
      *
      * <pre> # link
      * title
      * index</pre>
      *
      * @param uhsFileArray array of all available lines in the file
      * @param rawuhs array of raw bytes at the end of the file
      * @param rawOffset offset to the raw bytes from the beginning of the file
      * @param rootNode an existing root node
      * @param currentNode an existing node to add children to
      * @param key this file's hint decryption key
      * @param startIndex the line number to start parsing from
      * @return the number of lines consumed from the file in parsing children
      */

    public int ParseLinkNode(List<string> uhsFileArray, byte[] rawuhs, long rawOffset, UHSRootNode rootNode,
                             UHSNode currentNode, int[] key, int startIndex)
    {
      int index = startIndex;
      index++;
      // int innerCount = Int32.Parse(tmp.Substring(0, tmp.IndexOf(" "))) - 1;

      var newNode = new UHSNode("Link") {Content = uhsFileArray[index]};
      ParseTextEscapes(newNode);
      currentNode.Add(newNode);
      index++;

      int targetIndex = Int32.Parse(uhsFileArray[index]);
      newNode.LinkTarget = targetIndex;
      index++;

      //Removed since it ran endlessly when nodes link in both directions.
      //buildNodes(uhsFileArray, rawuhs, rawOffset, rootNode, newNode, key, targetIndex);

      return index - startIndex;
    }

    /**
     * Generates an image-filled UHSNode.
     * <br />The UHS format allows for regions that trigger links or reveal overlaid subimages.
     * <br />UHSHotSpotNode was written to handle regions.
     * <br />
     * <br />Illustrative UHS: <i>The Longest Journey: Chapter 7, the Stone Altar, Can you give me a picture of the solution?</i>
     * <br />Illustrative UHS: <i>Deja Vu I: Sewer, The Map</i>
     *
     * <pre> # hyperpng (or gifa)
     * title
     * 000000 offset Length
     * --not-a-gap--
     * x y x+w y+h
     * # link
     * title
     * index
     * --not-a-gap--
     * x y x+w y+h
     * # link
     * title
     * index
     * --not-a-gap--
     * x y x+w y+h
     * # overlay
     * title
     * 000000 offset Length x y
     * --not-a-gap--
     * x y x+w y+h
     * # overlay
     * title
     * 000000 offset Length x y</pre>
     *
     * <br />gifa has the same structure, but might not officially contain regions.
     *
     * @param uhsFileArray array of all available lines in the file
     * @param rawuhs array of raw bytes at the end of the file
     * @param rawOffset offset to the raw bytes from the beginning of the file
     * @param rootNode an existing root node
     * @param currentNode an existing node to add children to
     * @param key this file's hint decryption key
     * @param startIndex the line number to start parsing from
     * @return the number of lines consumed from the file in parsing children
     * @see org.openuhs.core.UHSHotSpotNode
     */

    public int ParseHyperImgNode(List<string> uhsFileArray, byte[] rawuhs, long rawOffset, UHSRootNode rootNode,
                                 UHSNode currentNode, int[] key, int startIndex)
    {
      int index = startIndex;
      var hotspotNode = new UHSHotSpotNode("HotSpot"); //This may or may not get used

      String tmp = uhsFileArray[index];
      index++;
      int innerCount = Int32.Parse(tmp.Substring(0, tmp.IndexOf(" "))) - 1;

      String type;
      if (tmp.IndexOf("hyperpng") != -1) type = "Hyperpng";
      else if (tmp.IndexOf("gifa") != -1) type = "Hypergif";
      else
      {
        MessageBox.Show(@"parseHyperImgNode() is for hyperpng and gifa hunks only.");
        index += innerCount;
        return index - startIndex;
      }

      var imgNode = new UHSNode(type);
      String title = uhsFileArray[index];
      index++;
      innerCount--;

      String[] tokens = (uhsFileArray[index]).Split(' ');
      index++;
      innerCount--;
      if (tokens.Length != 3)
        return innerCount + 3;
      //Skip dummy zeroes
      long offset = long.Parse(tokens[1]) - rawOffset;
      int length = Int32.Parse(tokens[2]);
      byte[] tmpBytes = null;
      if (rawOffset != -1) tmpBytes = ReadBinaryHunk(rawuhs, offset, length);
      if (tmpBytes == null)
      {
        MessageBox.Show(@"Could not read referenced raw bytes");
      }
      imgNode.Content = BytesToString(tmpBytes);

      //This if-else would make regionless hyperimgs standalone and unnested
      //if (innerCount+3 > 3) {
      hotspotNode.Add(imgNode);

      hotspotNode.Content = title;
      hotspotNode.Id = startIndex;
      currentNode.Add(hotspotNode);
      rootNode.AddLink(hotspotNode);
      //} else {
      //  imgNode.Id = startIndex);
      //  currentNode.Add(imgNode);
      //  rootNode.AddLink(imgNode);
      //}


      for (int j = 0; j < innerCount;)
      {
        tokens = (uhsFileArray[index + j]).Split(' ');
        j++;
        if (tokens.Length != 4)
          return innerCount + 3;
        int zoneX1 = Int32.Parse(tokens[0]) - 1;
        int zoneY1 = Int32.Parse(tokens[1]) - 1;
        int zoneX2 = Int32.Parse(tokens[2]) - 1;
        int zoneY2 = Int32.Parse(tokens[3]) - 1;

        tmp = uhsFileArray[index + j];
        j++;
        var regex = new Regex("[0-9]+ [A-Za-z]+$");
        if (regex.Match(tmp).Success)
        {
          int innerInnerCount = Int32.Parse(tmp.Substring(0, tmp.IndexOf(" "))) - 1;
          if (tmp.EndsWith("overlay"))
          {
            j++;
            tokens = (uhsFileArray[index + j]).Split(' ');
            j++;

            if (tokens.Length != 5)
              return innerCount + 3;
            //Skip dummy zeroes
            offset = long.Parse(tokens[1]) - rawOffset;
            length = Int32.Parse(tokens[2]);
            int posX = Int32.Parse(tokens[3]) - 1;
            int posY = Int32.Parse(tokens[4]) - 1;

            var newNode = new UHSNode("Overlay");
            tmpBytes = null;
            if (rawOffset != -1) tmpBytes = ReadBinaryHunk(rawuhs, offset, length);
            if (tmpBytes == null)
            {
              MessageBox.Show(@"Could not read referenced raw bytes");
            }
            newNode.Content = BytesToString(tmpBytes);
            hotspotNode.Add(newNode);
            hotspotNode.Coords(newNode, new[] {zoneX1, zoneY1, zoneX2 - zoneX1, zoneY2 - zoneY1, posX, posY});
          }
          else if (tmp.EndsWith("link"))
          {
            var newNode = new UHSNode("Link") {Content = uhsFileArray[index + j]};
            ParseTextEscapes(newNode);
            hotspotNode.Add(newNode);
            hotspotNode.Coords(newNode, new[] {zoneX1, zoneY1, zoneX2 - zoneX1, zoneY2 - zoneY1, -1, -1});
            j++;
            int targetIndex = Int32.Parse(uhsFileArray[index + j]);
            newNode.LinkTarget = targetIndex;
            j++;
          }
          else
          {
            j += innerInnerCount - 1;
          }
        }
        else
        {
          j++;
        }
      }
      index += innerCount;
      return index - startIndex;
    }


    /**
     * Generates a sound UHSNode.
     * <br />This seems to be limited to PCM WAV audio.
     * <br />
     * <br />Illustrative UHS: Tex Murphy: Overseer: Day Two, Bosworth Clark's Lab, How do I operate that keypad?
     *
     * <pre> # sound
     * title
     * 000000 offset Length</pre>
     *
     * @param uhsFileArray array of all available lines in the file
     * @param rawuhs array of raw bytes at the end of the file
     * @param rawOffset offset to the raw bytes from the beginning of the file
     * @param rootNode an existing root node
     * @param currentNode an existing node to add children to
     * @param key this file's hint decryption key
     * @param startIndex the line number to start parsing from
     * @return the number of lines consumed from the file in parsing children
     * @see #decryptTextHunk(String, int[]) decryptTextHunk(String, int[])
     */

    public int ParseSoundNode(List<string> uhsFileArray, byte[] rawuhs, long rawOffset, UHSRootNode rootNode,
                              UHSNode currentNode, int[] key, int startIndex)
    {
      int index = startIndex;
      index++;
      // int innerCount = Int32.Parse(tmp.Substring(0, tmp.IndexOf(" "))) - 1;

      var soundNode = new UHSNode("Sound") {Content = uhsFileArray[index]};
      ParseTextEscapes(soundNode);
      soundNode.Id = startIndex;
      currentNode.Add(soundNode);
      rootNode.AddLink(soundNode);
      index++;

      string tmp = uhsFileArray[index];
      index++;
      long offset = long.Parse(tmp.Substring(tmp.IndexOf(" ") + 1, tmp.LastIndexOf(" ") - tmp.IndexOf(" ") + 1)) -
                    rawOffset;
      int length = Int32.Parse(tmp.Substring(tmp.LastIndexOf(" ") + 1));

      var newNode = new UHSNode("SoundData");

      byte[] tmpBytes = null;
      if (rawOffset != -1) tmpBytes = ReadBinaryHunk(rawuhs, offset, length);
      if (tmpBytes == null)
      {
        MessageBox.Show(@"Could not read referenced raw bytes");
      }

      newNode.Content = BytesToString(tmpBytes);
      soundNode.Add(newNode);

      return index - startIndex;
    }
  }
}